import { fabric } from 'fabric'

declare module 'fabric' {
  namespace fabric {
    interface Path{
      path: Array<string|number>[]
    }
    interface Object {
      selectable?: boolean
      __selectable?: boolean
      hoverCursor?: string
      __hoverCursor?: string
    }
    interface Canvas {
      __selection?: boolean
      off(eventName?: string | any, handler?: (e: IEvent<any>) => void): any;
    }
  }
}

interface PathPoint {
  current: fabric.Point
  prevControl?: fabric.Point
  nextControl?: fabric.Point
}

class PenTool {
  public isOpen: boolean = false
  private onDownCanvas
  private onMoveCanvas
  private onUpCanvas
  private _activePathObj: fabric.Path | null = null
  private __isDestroyed = false
  private isDowning = false
  private isClosedPath = false
  private readonly PATH_NAME = 'fabricPenToolPathName'
  private readonly controlRadius = 4
  private _pathNodeCircles: fabric.Circle[] = []
  private _pathNodeControls: Array<Array<fabric.Circle | fabric.Line> | undefined> = []
  private _tipsObj: fabric.Text| null = null
  public subColor = '#666'
  private _pathPoints: PathPoint[] = []
  private activePathPointIndex: number = -1
  constructor(public canvas: fabric.Canvas, public pathStyle?: fabric.IPathOptions) {
    this.onDownCanvas = this._onDownCanvas.bind(this)

    this.canvas.__selection = this.canvas.selection
    this.onMoveCanvas = this._onMoveCanvas.bind(this)
    this.onUpCanvas = this._onUpCanvas.bind(this)
    canvas.on('mouse:down', this.onDownCanvas)
    canvas.on('mouse:move', this.onMoveCanvas)
    canvas.on('mouse:up', this.onUpCanvas)
  }

  public get isDestroyed() {
    return this.__isDestroyed
  }

  set isDestroyed(val: boolean) {
    throw new Error('This property cannot be set manually.')
  }

  private setTipsObj(text?: string, point?: fabric.Point) {
    if (text && point && this._tipsObj) {
      // Modify
      this._tipsObj.set({
        top: point.y,
        left: point.x,
        text: text
      })
    } else if(point && text) {
      this._tipsObj = this.makeText(text, { top: point.y, left: point.x, fontSize: 12 })
      this.canvas.add(this._tipsObj)
    } else if (this._tipsObj) {
      this.canvas.remove(this._tipsObj)
      this._tipsObj = null
    }
    this.canvas.requestRenderAll()
  }

  private get pathPoints() {
    return this._pathPoints
  }

  private set pathPoints(points: PathPoint[]) {
    // 进行监听以响应操作
    this.drawPath(points)

    this._pathPoints = points
  }
  // 目前只支持新增，不支持删除和拖动点
  drawPath(curPoints: PathPoint[]) {
    if (curPoints.length === 0) {
      const objs: fabric.Object[] = [...this._pathNodeCircles]
      this._pathNodeControls.forEach(item => {
        item && objs.push(...item)
      })
      this.canvas.remove(...objs)
      this.canvas.requestRenderAll()
      return 
    }
    // 生成
    if (this._activePathObj) {
      
      this._activePathObj.path = this.makePathArr(curPoints)
      // draw control line and node circle
      curPoints.forEach((item, i, arr) => {
        const pathNodeCircle = this._pathNodeCircles[i]
        const pathNodeControl = this._pathNodeControls[i]
        // draw node control line
        if (pathNodeControl) {
          this.canvas.remove(...pathNodeControl)
          if (i < arr.length - 2) {
            pathNodeControl.splice(0, pathNodeControl.length)
          } else {
            const cl = this.makeControlLine(item.current, item.prevControl, item.nextControl)
            if (cl) {
              pathNodeControl.splice(0, pathNodeControl.length, ...cl)
              this.canvas.add(...pathNodeControl)
            }
          }
        } else if(!pathNodeControl && item?.prevControl && item?.nextControl) {
          const cl = this.makeControlLine(item.current, item.prevControl, item.nextControl)
          this._pathNodeControls[i] = cl
          cl && this.canvas.add(...cl)
        }

        // draw node circle
        if (pathNodeCircle && (pathNodeCircle.top !== item.current.y || pathNodeCircle.left !== item.current.x)) {
          pathNodeCircle.set({
            top: item.current.y,
            left: item.current.x
          })
        } else if(!pathNodeCircle) {
          this._pathNodeCircles.push(this.makeCircle(item.current))
          this.canvas.add(this._pathNodeCircles[this._pathNodeCircles.length - 1])
        }
      })
    } else {
      this._activePathObj = new fabric.Path(`M ${curPoints[0]?.current.x} ${curPoints[0]?.current.y}`, { fill: '', stroke: '#000', ...this.pathStyle, objectCaching: false, selectable: false, hoverCursor: 'default' })
      this._pathNodeControls = curPoints[0]?.prevControl && curPoints[0]?.nextControl ? [this.makeControlLine(curPoints[0].current, curPoints[0].prevControl, curPoints[0].nextControl)]: []
      this._pathNodeCircles = [this.makeCircle(curPoints[0]?.current)]
      this.canvas.add(this._activePathObj, this._pathNodeCircles[0])
      this._pathNodeControls[0] && this.canvas.add(...this._pathNodeControls[0])
    }
    this.canvas.requestRenderAll()
  }

  _onDownCanvas(opt: fabric.IEvent<MouseEvent>) {
    if (!this.isOpen) return
    let { x, y } = this.canvas.getPointer(opt.e)
    if (this._activePathObj) {

      if (this.canClosePath(x, y)) {
        this.pathPoints = [...this.pathPoints, { current: this.pathPoints[0].current }]
        this.isClosedPath = true
      } else if (this.canBreakPath(x, y, opt.e.altKey)) {
        this.pathPoints = this.pathPoints.map((item, index, arr) => {
          if (index === arr.length - 1) {
            return {
              ...item,
              nextControl: undefined
            }
          }
          return item
        })
      } else if (this.canDonePath(x, y)) {
        this.complete()
      } else {
        this.pathPoints = [...this.pathPoints, { current: new fabric.Point(x, y) }]
      }
      
      this.activePathPointIndex = this.pathPoints.length - 1
    } else {
      this.pathPoints = [{ current: new fabric.Point(x, y) }]
      this.activePathPointIndex = 0
    }
    this.isDowning = true
  }

  _onMoveCanvas(opt: fabric.IEvent<MouseEvent>) {
    if (this._activePathObj) {
      const { x, y } = this.canvas.getPointer(opt.e)
      if (this.isDowning) {
        const controlPoint = this.getSymmetryPoint(this.pathPoints[this.activePathPointIndex].current, new fabric.Point(x, y))
        this.pathPoints = this.pathPoints.map((item, i) => {
          if (this.activePathPointIndex === i) {
            return { current: this.pathPoints[this.activePathPointIndex].current, prevControl: controlPoint, nextControl: new fabric.Point(x, y) }
          }
          return item
        })
      }

      if (this.canClosePath(x, y)) {
        this.setTipsObj('close', new fabric.Point(x + 14, y + 8))
      } else if (this.canBreakPath(x, y, opt.e.altKey)) {
        this.setTipsObj('break', new fabric.Point(x + 14, y + 8))
      } else if (this.canDonePath(x, y)) {
        this.setTipsObj('done', new fabric.Point(x + 14, y + 8))
      } else {
        this._tipsObj && this.setTipsObj()
      }
      
    }
  }

  _onUpCanvas(opt: fabric.IEvent<MouseEvent>) {
    
    if (this.isDowning) {
      if (this.isClosedPath) {
        this.complete()
      }
      this.isDowning = false
      this.isClosedPath = false
      this.canvas.requestRenderAll()
    }
  }

  open() {
    if (this.isDestroyed) throw new Error('PenTool instance has been destroyed!')
    this.isOpen = true
    this.canvas.selection && (this.canvas.selection = false)
    this.canvas.discardActiveObject()
    this.canvas.getObjects().forEach(obj => {
      obj.__selectable = obj.selectable
      obj.__hoverCursor = obj.hoverCursor
      obj.selectable = false
      obj.hoverCursor = 'default'
    })
    this.canvas.requestRenderAll()
    // console.log('开启钢笔工具')
  }

  close() {
    if (this.isDestroyed) throw new Error('PenTool instance has been destroyed!')
    this.isOpen = false
    // 如果当前绘画未完成，则先完成
    this._activePathObj && this.complete()
    this.resetObjSelectable()
    this.canvas.requestRenderAll()
    this.canvas.selection = this.canvas.__selection
    // console.log('关闭钢笔工具')
  }

  destroy() {
    if (this.isDestroyed) throw new Error('PenTool instance has been destroyed!')
    this.isOpen = false
    // 如果当前绘画未完成，则先完成
    this._activePathObj && this.complete()
    // 把path复原
    this.resetObjSelectable()
    this.canvas.requestRenderAll()
    this.canvas.selection = this.canvas.__selection
    this._activePathObj = null
    // 移除所有已注册的事件
    this.canvas.off('mouse:down', this.onDownCanvas)
    this.__isDestroyed = true
  }

  canClosePath(x: number, y: number) {
    let ltX, ltY, rbX, rbY, cX, cY
    if (this.pathPoints.length < 2) {
      return false
    }
    cX = this.pathPoints[0].current.x
    cY = this.pathPoints[0].current.y

    ltX = cX - this.controlRadius
    ltY = cY - this.controlRadius
    rbX = cX + this.controlRadius
    rbY = cY + this.controlRadius
    
    return x >= ltX && y >= ltY && x <= rbX && y <= rbY
  }

  canRemovePath(x: number, y: number) {
    return false
  }

  canDonePath(x: number, y: number) {
    let ltX, ltY, rbX, rbY, cX, cY
    if (this.pathPoints.length < 2) {
      return false
    }
    cX = this.pathPoints[this.pathPoints.length - 1].current.x
    cY = this.pathPoints[this.pathPoints.length - 1].current.y

    ltX = cX - this.controlRadius
    ltY = cY - this.controlRadius
    rbX = cX + this.controlRadius
    rbY = cY + this.controlRadius
    return x >= ltX && y >= ltY && x <= rbX && y <= rbY
  }

  canBreakPath(x: number, y: number, altKey=false) {
    let ltX, ltY, rbX, rbY, cX, cY
    if (this.pathPoints.length === 0 || !altKey) {
      return false
    }
    cX = this.pathPoints[this.pathPoints.length - 1].current.x
    cY = this.pathPoints[this.pathPoints.length - 1].current.y

    ltX = cX - this.controlRadius
    ltY = cY - this.controlRadius
    rbX = cX + this.controlRadius
    rbY = cY + this.controlRadius
    return x >= ltX && y >= ltY && x <= rbX && y <= rbY
  }

  makePathArr(points: PathPoint[]) {
    const pathArr: Array<string|number>[] = []

    points.forEach((item, i, arr) => {
      let token = []
      if (i === 0) {
        token = ['M', item.current.x, item.current.y]
      } else {
        // L or Q or C
        const prevItem = arr[i - 1]
        if (prevItem?.nextControl && item.prevControl) {
          token = ['C', prevItem.nextControl.x, prevItem.nextControl.y, item.prevControl.x, item.prevControl.y, item.current.x, item.current.y]
        } else if(prevItem?.nextControl) {
          token = ['Q', prevItem.nextControl.x, prevItem.nextControl.y, item.current.x, item.current.y]
        } else if(item?.prevControl) {
          token = ['Q', item.prevControl.x, item.prevControl.y, item.current.x, item.current.y]
        } else {
          // none
          token = ['L', item.current.x, item.current.y]
        }
      }
      pathArr.push(token)
    })

    if (points[0].current.eq(points[points.length - 1].current)) {
      pathArr.push(['Z'])
    }

    return pathArr
  }

  makePathObj(paths: Array<string|number>[], pathStyle?: fabric.IPathOptions) {
    const pathStr = paths.reduce((str, curPaths) => str += curPaths.join(' '), '')
    return new fabric.Path(pathStr, pathStyle)
  }

  makeControlLine(mp: fabric.Point, sp?: fabric.Point, ep?: fabric.Point) {
    if (sp && ep) {
      return [
        this.makeLine(sp, mp),
        this.makeLine(mp, ep),
        this.makeCircle(sp),
        this.makeCircle(mp),
        this.makeCircle(ep),
      ]
    } else if (sp) {
      return [
        this.makeLine(sp, mp),
        this.makeCircle(sp),
        this.makeCircle(mp),
      ]
    } else if (ep) {
      return [
        this.makeLine(mp, ep),
        this.makeCircle(mp),
        this.makeCircle(ep),
      ]
    }
  }

  makeText(text: string, options?: fabric.ITextOptions) {
    return new fabric.Text(text, { fill: this.subColor || '#666', ...options })
  }

  makeCircle(point: fabric.Point, options?: fabric.ICircleOptions) {
    return new fabric.Circle({
      fill: this.subColor || '#666',
      top: point.y,
      left: point.x,
      radius: this.controlRadius || 4,
      originX: 'center',
      originY: 'center',
      selectable: false,
      hoverCursor: 'default',
      ...options
    })
  }

  makeLine(sp: fabric.Point, ep: fabric.Point, options?: fabric.ILineOptions) {
    return new fabric.Line([
      sp.x, sp.y, ep.x, ep.y 
    ], {
      stroke: this.subColor || '#666',
      strokeWidth: 1,
      selectable: false,
      evented: false,
      hoverCursor: 'default',
      ...options
    })
  }

  getSymmetryPoint(midPoint: fabric.Point, controlPoint: fabric.Point) {
    return midPoint.multiply(2).subtract(controlPoint)
  }

  resetObjSelectable() {
    // 把path复原
    this.canvas.getObjects().forEach(obj => {
      if (obj.name !== this.PATH_NAME) {
        obj.selectable = obj.__selectable
        obj.hoverCursor = obj.__hoverCursor
      } else {
        obj.selectable = true
        obj.hoverCursor = undefined
      }

    })
  }

  complete() {
    if (!this._activePathObj) return
    this.canvas.remove(this._activePathObj)
    this.canvas.add(this.makePathObj(this._activePathObj.path, { fill: '', stroke: '#000', ...this.pathStyle, selectable: false, hoverCursor: 'default', name: this.PATH_NAME }))
    this.canvas.requestRenderAll()
    this._activePathObj = null
    this.pathPoints = []
    this.setTipsObj()
  }

}

export { PenTool }
